﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright company="Microsoft Corp." file="DeploymentManager.cs">
//   All rights reserved. See License.txt in the project root for license information.
// </copyright>
// 
// --------------------------------------------------------------------------------------------------------------------
namespace CloudBlitz.DeploymentManager
{
    using System;
    using System.Collections.Generic;
    using System.Security.Cryptography.X509Certificates;

    using CloudBlitz.DeploymentManager.DeploymentOperations;
    using CloudBlitz.DeploymentManager.DeploymentWorkflow;

    using Common;

    using Microsoft.WindowsAzure.ServiceRuntime;

    public class DeploymentManager
    {
        private const int InitializeDbTimeoutInMilliseconds = 1000000;

        private const int StabilizeDeploymentTimeoutInMinutes = 30;

        private readonly IAzureServiceClientsFactory azureServiceClientsFactory;

        private readonly Func<SqlAzureServerCredentials, ISqlAzureServerManager> sqlAzureServerManagerFactory;

        private readonly Func<LocalResource> localResourceFactory;

        private readonly ILogger logger;

        private DeploymentState deploymentState = DeploymentState.Configuring;

        public DeploymentManager(
                        IAzureServiceClientsFactory azureServiceClientsFactory, 
                        Func<SqlAzureServerCredentials, ISqlAzureServerManager> sqlAzureServerManagerFactory, 
                        Func<LocalResource> localResourceFactory, 
                        ILogger logger)
        {
            this.azureServiceClientsFactory = azureServiceClientsFactory;
            this.sqlAzureServerManagerFactory = sqlAzureServerManagerFactory;
            this.localResourceFactory = localResourceFactory;
            this.logger = logger;
        }

        public void DeployCluster(DeploymentData deploymentData)
        {
            this.ChangeStateAndReport(0, "Configuring", DeploymentState.Configuring);

            var deploymentContext = this.CreateDeploymentContext(deploymentData);

            var deployWorkflowWeight = 0.9d;
            var deployWorkflow = this.CreateDeployWorkflow();
            deployWorkflow.ProgressChanged += (s, a) => this.ReportState(Convert.ToInt32(a.Progress * deployWorkflowWeight), a.Message, this.deploymentState);

            var rollbackWorkflow = this.CreateRollbackWorkflow();
            rollbackWorkflow.ProgressChanged += (s, a) => this.ReportState(Convert.ToInt32((a.Progress *  (1 - deployWorkflowWeight)) + 100 * deployWorkflowWeight), a.Message, this.deploymentState);

            this.ChangeStateAndReport(0, "Running deploy workflow", DeploymentState.Running);
            if (deployWorkflow.Execute(deploymentContext).Status == DeploymentOperationStatus.Failed)
            {
                this.ChangeStateAndReport(Convert.ToInt32(deployWorkflowWeight * 100), "An error occured while deploying hpc cluster, rolling back changes", DeploymentState.Cancelling);

                rollbackWorkflow.Execute(deploymentContext);

                this.ChangeStateAndReport(100, "Deployment cancelled", DeploymentState.Cancelled);
            }
            else
            {
                this.ChangeStateAndReport(100, "Deployment finished", DeploymentState.Finished);
            }
        }

        private DeploymentContext CreateDeploymentContext(DeploymentData deploymentData)
        {
            var serviceCertificate = new X509Certificate2(
                                                          Convert.FromBase64String(deploymentData.ServiceCertificate),
                                                          deploymentData.AdministratorPassword,
                                                          X509KeyStorageFlags.Exportable | X509KeyStorageFlags.DefaultKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.MachineKeySet);

            return new DeploymentContext
            {
                AzureCredentials = new AzureCredentials
                {
                    SubscriptionId = deploymentData.SubscriptionId,
                    CertificateThumbprint = deploymentData.ManagementCertificateThumbprint
                },
                UserStorageConnectionString = deploymentData.StorageConnectionString,
                ServiceCertificate = serviceCertificate,
                ServiceCertificatePassword = deploymentData.AdministratorPassword,
                ClusterPackageDetails = new AzurePackageDetails
                {
                    CspkgFile = deploymentData.ClusterTemplatePackage,
                    CsdefFile = deploymentData.ClusterTemplateCsdef,
                    CscfgFile = deploymentData.ClusterTemplateCscfg
                },
                HostedServiceName = deploymentData.ClusterDnsName,
                Location = deploymentData.ClusterLocation,
                SqlServerDetails = new SqlServerDetails
                {
                    DatabaseName = deploymentData.ClusterDnsName,
                    MaxSizeInGb = 5,
                    Name = string.Empty,
                    Credentials = new SqlAzureServerCredentials
                    {
                        AdministratorLogin = deploymentData.AdministratorLogin,
                        AdministratorPassword = deploymentData.AdministratorPassword
                    }
                },
                StorageAccountName = deploymentData.ClusterDnsName,
                ClusterDetails = new ClusterDetails
                {
                    Name = deploymentData.ClusterDnsName,
                    SSLCertificate = serviceCertificate,
                    PasswordEncryptionCertificate = serviceCertificate,
                    ClusterAdmin = deploymentData.AdministratorLogin,
                    ClusterAdminPassword = deploymentData.AdministratorPassword,
                    HeadNodeName = "HeadNode",
                    ComputeNodeName = "ComputeNode",
                    HeadNodeInstanceCount = deploymentData.HeadNodeInstanceCount,
                    ComputeNodeInstanceCount = deploymentData.ComputeNodeInstanceCount
                }
            };
        }

        private IDeploymentOperation CreateDeployWorkflow()
        {
            var copyFilesOperation = new CopyFilesOperation(this.localResourceFactory);
            var createSqlServerOperation = new InitializeSqlServerOperation(this.azureServiceClientsFactory, this.sqlAzureServerManagerFactory);
            var createDatabaseOperation = new CreateDatabaseOperation(this.sqlAzureServerManagerFactory);
            var initializeDbOperation = new InitializeDatabaseOperation(true, InitializeDbTimeoutInMilliseconds);
            var generateCscfgOperation = new GenerateClusterConfigFileOperation();
            var initializeStorageAccountOperation = new InitializeStorageAccountOperation(this.azureServiceClientsFactory);
            var initializeHostedServiceOperation = new InitializeHostedServiceOperation(this.azureServiceClientsFactory);
            var addServiceCertificateOperation = new AddServiceCertificateOperation(this.azureServiceClientsFactory);
            var createDeploymentOperation = new CreateDeploymentOperation(this.azureServiceClientsFactory);
            var stabilizeDeploymentOperation = new WaitForDeploymentToStabilizeOperation(this.azureServiceClientsFactory, TimeSpan.FromMinutes(StabilizeDeploymentTimeoutInMinutes));

            var createSqlServerAndDbOperation = new SerialOperation(new List<IDeploymentOperation>
            {
                createSqlServerOperation,
                createDatabaseOperation
            });

            var initializeStorageOperation = new ParallelOperation(new List<IDeploymentOperation>
            {
                copyFilesOperation,
                createSqlServerAndDbOperation,
                initializeStorageAccountOperation
            });

            var initializeOperationPart1 = new SerialOperation(new List<IDeploymentOperation>
            {
                initializeStorageOperation,
                generateCscfgOperation,
                initializeDbOperation
            });

            var initializeOperationPart2 = new SerialOperation(new List<IDeploymentOperation>
            {
                initializeHostedServiceOperation,
                addServiceCertificateOperation
            });

            var initializeOperation = new ParallelOperation(new List<IDeploymentOperation>
            {
                initializeOperationPart1,
                initializeOperationPart2
            });
            
            return new SerialOperation(new List<IDeploymentOperation>
            {
                initializeOperation,
                createDeploymentOperation,
                stabilizeDeploymentOperation
            });
        }

        private IDeploymentOperation CreateRollbackWorkflow()
        {
            var deleteDeploymentOperation = new DeleteDeploymentOperation(this.azureServiceClientsFactory);
            var deleteHostedServiceOperation = new DeleteHostedServiceOperation(this.azureServiceClientsFactory);
            var deleteSqlServerOperation = new DeleteSqlServerOperation(this.azureServiceClientsFactory);

            var removeHostedServiceOperation = new SerialOperation(new List<IDeploymentOperation>
            {
                deleteDeploymentOperation,
                deleteHostedServiceOperation
            });

            var rollbackOperation = new ParallelOperation(new List<IDeploymentOperation>
            {
                removeHostedServiceOperation,
                deleteSqlServerOperation
            });

            return rollbackOperation;
        }

        private void ChangeStateAndReport(int progress, string message, DeploymentState state)
        {
            this.deploymentState = state;
            this.ReportState(progress, message, this.deploymentState);
        }

        private void ReportState(int progress, string message, DeploymentState state)
        {
            var deploymentProgress = new DeploymentProgress(progress, message, state);
            this.logger.LogInformation(deploymentProgress.SerializeToJson());
        }
    }
}